use std::{collections::{hash_map::Entry, VecDeque}, hash, sync::Arc};

use indexmap::IndexMap;
use log::debug;
use xelis_vm::{
    Context,
    EnvironmentError,
    FnInstance,
    FnParams,
    FnReturnType,
    Primitive,
    Reference,
    SysCallResult,
    traits::{JSONHelper, Serializable}
};
use crate::{
    contract::{
        from_context,
        has_enough_balance_for_contract,
        record_balance_charge,
        record_balance_credit,
        ContractProvider,
        ContractMetadata,
        ModuleMetadata,
        ContractModule
    },
    crypto::Hash,
    transaction::ContractDeposit
};

#[derive(Clone, Debug)]
pub struct OpaqueContract {
    // Contract hash
    pub hash: Hash,
    // Actual module
    pub contract_module: ContractModule,
}

impl PartialEq for OpaqueContract {
    fn eq(&self, other: &Self) -> bool {
        self.hash == other.hash
    }
}

impl Eq for OpaqueContract {}

impl hash::Hash for OpaqueContract {
    fn hash<H: hash::Hasher>(&self, state: &mut H) {
        self.hash.hash(state);
    }
}

impl Serializable for OpaqueContract {}

impl JSONHelper for OpaqueContract {}

pub async fn contract_new<'a, 'ty, 'r, P: ContractProvider>(_: FnInstance<'a>, mut params: FnParams, _: &ModuleMetadata<'_>, context: &mut Context<'ty, 'r>) -> FnReturnType<ContractMetadata> {
    let (provider, state) = from_context::<P>(context)?;

    let contract: Hash = params.remove(0)
        .into_owned()
        .into_opaque_type()?;

    // Load the module from the provider
    let module = match state.modules.entry(contract.clone()) {
        Entry::Occupied(entry) => entry.into_mut(),
        Entry::Vacant(entry) => {
            let module = provider.load_contract_module(&contract, state.topoheight).await?;
            entry.insert(module)
        }
    }.clone();

    let Some(module) = module else {
        return Ok(SysCallResult::Return(Primitive::Null.into()));
    };

    let opaque = OpaqueContract {
        contract_module: module,
        hash: contract,
    };
    Ok(SysCallResult::Return(opaque.into()))
}

pub async fn contract_call<'a, 'ty, 'r, P: ContractProvider>(zelf: FnInstance<'a>, mut params: FnParams, metadata: &ModuleMetadata<'_>, context: &mut Context<'ty, 'r>) -> FnReturnType<ContractMetadata> {
    let zelf = zelf?;
    let opaque: &OpaqueContract = zelf.as_opaque_type()?;

    let (provider, chain_state) = from_context::<P>(context)?;

    let assets = params.remove(2)
        .into_owned()
        .to_map()?;

    let p = params.remove(1)
        .into_owned()
        .to_vec()?;

    if p.len() > (u8::MAX - 1) as usize {
        return Err(EnvironmentError::Static("Too many parameters"));
    }

    let chunk_id = params.remove(0)
        .into_owned()
        .as_u16()?;

    if !opaque.contract_module.module.is_public_chunk(chunk_id as usize) {
        return Err(EnvironmentError::Static("Chunk is not public"));
    }

    // Check if we have permission to call this contract
    if !chain_state.permission.allows(&opaque.hash, chunk_id) {
        return Err(EnvironmentError::Static("Permission denied to call this contract"));
    }

    let mut deposits = IndexMap::new();
    for (mut k, v) in assets {
        let asset: Hash = k.into_opaque_type()?;
        let amount = v.as_ref().as_u64()?;

        if amount == 0 {
            return Err(EnvironmentError::Static("amount is zero"));
        }

        if !has_enough_balance_for_contract(provider, chain_state, metadata.metadata.contract_executor.clone(), asset.clone(), amount).await? {
            return Err(EnvironmentError::Static("Insufficient funds for deposit"));
        }

        // Insert the deposit to the called contract
        record_balance_charge(provider, chain_state, metadata.metadata.contract_executor.clone(), asset.clone(), amount).await?;
        record_balance_credit(provider, chain_state, opaque.hash.clone(), asset.clone(), amount).await?;

        debug!("Transfering {} of {} to {} from {}", amount, asset, opaque.hash, metadata.metadata.contract_executor);
        deposits.insert(asset, ContractDeposit::Public(amount));
    }

    let p = p.into_iter()
        .map(|v| v.to_owned().into())
        .collect::<VecDeque<_>>();

    Ok(SysCallResult::ModuleCall {
        module: opaque.contract_module.module.clone(),
        metadata: Arc::new(ContractMetadata {
            contract_executor: opaque.hash.clone(),
            contract_caller: Some(metadata.metadata.contract_executor.clone()),
            deposits,
        }),
        environment: None,
        chunk: chunk_id,
        params: p,
    })
}

pub async fn contract_delegate<'a, 'ty, 'r>(zelf: FnInstance<'a>, mut params: FnParams, metadata: &ModuleMetadata<'_>, _: &mut Context<'ty, 'r>) -> FnReturnType<ContractMetadata> {
    let zelf = zelf?;
    let opaque: &OpaqueContract = zelf.as_opaque_type()?;
    let p = params.remove(1)
        .into_owned()
        .to_vec()?
        .into_iter()
        .map(|v| v.to_owned().into())
        .collect::<VecDeque<_>>();

    let chunk_id = params.remove(0)
        .into_owned()
        .as_u16()?;

    if !opaque.contract_module.module.is_public_chunk(chunk_id as usize) {
        return Err(EnvironmentError::Static("Chunk is not public"));
    }

    Ok(SysCallResult::ModuleCall {
        module: opaque.contract_module.module.clone(),
        // Reuse the metadata from the module
        metadata: match &metadata.metadata {
            Reference::Borrowed(v) => Arc::new((**v).clone()),
            Reference::Shared(v) => v.clone(),
        },
        // TODO: fetch the environment when we have a different one
        environment: None,
        chunk: chunk_id,
        params: p,
    })
}

pub fn contract_get_hash<'a>(zelf: FnInstance<'a>, _: FnParams, _: &ModuleMetadata<'_>, _: &mut Context<'_, '_>) -> FnReturnType<ContractMetadata> {
    let zelf = zelf?;
    let opaque: &OpaqueContract = zelf.as_opaque_type()?;

    Ok(SysCallResult::Return(opaque.hash.clone().into()))
}